﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows.Browser;
using System.Windows.Threading;
using System.Collections.ObjectModel;

namespace SuperDeepZoom
{
    /// <summary>
    /// This class helps layout the images in the MultiScaleImage
    /// It performs scaling, animation, rearranging etc.
    /// Basicaly it does all the cool stuff :)
    /// It also handles zoom and detects which image is currently being viewed.
    /// </summary>
    internal class ImageArranger : IDisposable
    {
        static int storyboardKeyIndex = 1;

        MultiScaleImage msi;

        Point lastMousePos = new Point();

        List<MultiScaleSubImage> imagesToShow;
        List<MultiScaleSubImage> imagesToHide;

        int? viewedImage;

        public event EventHandler ViewedImageChanged;
        public event EventHandler ArrangeAnimationComplete;
        public event EventHandler ImagesRearranged;

        public ImageArranger(MultiScaleImage msi)
        {
            this.msi = msi;
            viewedImage = null;

            msi.ViewportChanged += new RoutedEventHandler(msi_ViewportChanged);
        }

        /// <summary>
        /// Detect if an image has been clicked
        /// </summary>
        /// <param name="p">The point which is to be tested</param>
        public void HitTestAndZoomOnImage(Point p)
        {
            int subImageIndex = subImageHitTest(p);
            if (subImageIndex >= 0)
            {
                DisplaySubImageCentered(subImageIndex, false);
            }
        }

        /// <summary>
        /// Zoom the image in or out depending on the zoom factor
        /// </summary>
        /// <param name="zoom">Factor to zoom by</param>
        public void Zoom(double zoom)
        {
            if ((zoom >= 1.0 && msi.ViewportWidth > 0.01) || (zoom < 1.0 && msi.ViewportWidth < 2))
            {
                Point logicalPoint = this.msi.ElementToLogicalPoint(lastMousePos);
                msi.ZoomAboutLogicalPoint(zoom, logicalPoint.X, logicalPoint.Y);
            }
        }

        public void Zoom(double zoom, Point logicalPoint)
        {
            if ((zoom >= 1.0 && msi.ViewportWidth > 0.01) || (zoom < 1.0 && msi.ViewportWidth < 2))
            {
                msi.ZoomAboutLogicalPoint(zoom, logicalPoint.X, logicalPoint.Y);
            }
        }

        /// <summary>
        /// Detect if an outer rectangle wholly contains an inner rectangle
        /// </summary>
        /// <param name="outer"></param>
        /// <param name="inner"></param>
        /// <returns></returns>
        bool rectInRect(Rect outer, Rect inner)
        {
            if (outer.Right > inner.Right && outer.Left < inner.Left)
            {
                if (outer.Top < inner.Top && outer.Bottom > inner.Bottom)
                {
                    return true;
                }
            }
            return false;
        }


        /// <summary>
        /// Detect it if the inner rect is inside the vertical position of the outer rect
        /// </summary>
        /// <param name="outer"></param>
        /// <param name="inner"></param>
        /// <returns></returns>
        bool rectInRectVertical(Rect outer, Rect inner)
        {
            if (outer.Top < inner.Top && outer.Bottom > inner.Bottom)
            {
                return true;
            }

            return false;
        }

        double distanceBetweenPoints(Point a, Point b)
        {
            double distance = Math.Sqrt(Math.Pow(b.X - a.X, 2) + Math.Pow(b.Y - a.Y, 2));
            return distance;
        }

        /// <summary>
        /// Increase the size of a rectangle by a given percentage
        /// </summary>
        /// <param name="input">The original Rect</param>
        /// <param name="percent">Percentage to increase by</param>
        /// <returns></returns>
        Rect grow(Rect input, double percent)
        {
            Rect result = new Rect(
                input.X - ((percent / 100) * input.Width),
                input.Y - ((percent / 100) * input.Height),
                input.Width + (((percent / 100) * input.Width) * 2),
                input.Height + (((percent / 100) * input.Height) * 2)
                );

            return result;
        }

        DispatcherTimer waitViewedTimer;

        /// <summary>
        /// Detects if an image is taking a prominent position in the view
        /// </summary>
        void detectZoomedImage()
        {
            int? previousViewed = viewedImage;

            //The rectangle of the MSI control
            Rect msiRect = new Rect(0, 0, msi.ActualWidth, msi.ActualHeight);

            Point centrePoint = new Point(msiRect.Width / 2, msiRect.Height / 2);

            viewedImage = null;

            for (int i = 0; i < imagesToShow.Count; i++)
            {
                MultiScaleSubImage subImage = imagesToShow[i];

                Rect subImageRect = GetSubImageScreenRect(subImage);

                //Increase the size of the rect so that it will encompass the msi control even if the sub image does not.
                Rect growRect = grow(subImageRect, 50);

                Point subImageRectCentre = new Point(subImageRect.Left + (subImageRect.Width / 2), subImageRect.Top + (subImageRect.Height / 2));
                double distance = distanceBetweenPoints(subImageRectCentre, centrePoint);

                if (rectInRect(growRect, msiRect) || (rectInRectVertical(growRect, msiRect) && distance < 100))
                {
                    if (distance < 0)
                    {
                        distance = -distance;
                    }   
                    int vI = msi.SubImages.IndexOf(subImage);
                    viewedImage = vI;
                    break;
                }
            }

            //Only fire the events if the image has actually changed
            if (previousViewed != viewedImage)
            {

                if (waitViewedTimer != null)
                {
                    waitViewedTimer.Stop();
                    waitViewedTimer.Tick -= new EventHandler(waitViewedTimer_Tick);
                }

                if (ViewedImageChanged != null)
                {
                    if (viewedImage != null && viewedImage.HasValue)
                    {
                        //if the new image is not null, then don't raise the new change immediately
                        //this is so that as the user is scolling fast over the msi, say in a back or next
                        //movement that changes rows, they will not see the details updates flick past                        

                        waitViewedTimer = new DispatcherTimer();
                        waitViewedTimer.Tick += new EventHandler(waitViewedTimer_Tick);
                        waitViewedTimer.Interval = TimeSpan.FromMilliseconds(500);
                        waitViewedTimer.Start();
                    }
                    else
                    {
                        //if the viewed image is nothing, then update it to empty immediately.
                        ViewedImageChanged(this, EventArgs.Empty);
                    }
                }
            }
        }

        void waitViewedTimer_Tick(object sender, EventArgs e)
        {
            waitViewedTimer.Stop();
            waitViewedTimer.Tick -= new EventHandler(waitViewedTimer_Tick);
            waitViewedTimer = null;
            ViewedImageChanged(this, EventArgs.Empty);
        }

        public void ZoomOutDefault()
        {
            Zoom(.8);
        }

        public void ZoomInDefault()
        {
            Zoom(1.2);
        }

        public void ArrangeImages(List<MultiScaleSubImage> imagesToShow, List<MultiScaleSubImage> imagesToHide)
        {
            this.imagesToHide = imagesToHide;
            this.imagesToShow = imagesToShow;
            ArrangeImages();
        }

        /// <summary>
        /// Provides the image arrange algorithm. Detects rows and colums and animates between old and new locations.
        /// </summary>
        public void ArrangeImages()
        {
            if (imagesToShow == null)
            {
                //An attempt has been made to arrange the images before images are ready to be arranged
                return;
            }

            lastMousePos = new Point(0, 0);
            msi.ViewportOrigin = new Point(0, 0);
            msi.ViewportWidth = 1.0;

            Storyboard moveStoryboard = initStoryboard();

            double containerAspectRatio = this.msi.ActualWidth / this.msi.ActualHeight;
            double spaceBetweenImages = 0.005;

            List<SubImage> subImages = new List<SubImage>();
            imagesToShow.ForEach(subImage => subImages.Add(new SubImage(subImage)));

            // Capture the total width of all images
            double totalImagesWidth = 0.0;
            subImages.ForEach(subImage => totalImagesWidth += subImage.Width);

            // Calculate the total number of rows required to display all the images
            int numRows = (int)Math.Sqrt((totalImagesWidth / containerAspectRatio) + 1);

            // Assign images to each row
            List<Row> rows = new List<Row>(numRows);
            for (int i = 0; i < numRows; i++)
                rows.Add(new Row(spaceBetweenImages));

            double widthPerRow = totalImagesWidth / numRows;
            double imagesWidth = 0;

            // Separate the images into rows. The total width of all images in a row should not exceed widthPerRow
            for (int i = 0, j = 0; i < numRows; i++, imagesWidth = 0)
            {
                while (imagesWidth < widthPerRow && j < subImages.Count)
                {
                    rows[i].AddImage(subImages[j]);
                    subImages[j].RowNum = i;
                    imagesWidth += subImages[j++].Width;
                }
            }

            // At this point in time the subimage height is 1 
            // If we assume that the total height is also 1 we need to scale the subimages to fit within a total height of 1
            // If the total height is 1, the total width is aspectRatio. Hence (aspectRatio)/(total width of all images in a row) is the scaling factor.
            // Added later: take into account spacing between images
            rows.ForEach(Row => Row.Scale(containerAspectRatio));

            // Calculate the total height, with space between images, of the images across all rows
            // Also adjust the colNum for each image
            double totalImagesHeight = (numRows - 1) * spaceBetweenImages;
            rows.ForEach(Row => totalImagesHeight += Row.Height);

            // The totalImagesHeight should not exceed 1. 
            // if it does, we need to scale all images by a factor of (1 / totalImagesHeight)
            if (totalImagesHeight > 1)
            {
                subImages.ForEach(subImage => subImage.Scale(1 / (totalImagesHeight + spaceBetweenImages)));
                totalImagesHeight = (numRows - 1) * spaceBetweenImages;
                rows.ForEach(Row => totalImagesHeight += Row.Height);
            }

            // Calculate the top and bottom margin
            double margin = (1 - totalImagesHeight) / 2;

            if (imagesToHide != null)
            {
                // First hide all the images that should not be displayed
                imagesToHide.ForEach(subImage =>
                {
                    //Do not use opacity for this as it slows down the animation after a few 
                    //arranges
                    subImage.ViewportWidth = 0;
                });
            }

            // Then display the displayable images to scale
            for (int i = 0; i < imagesToShow.Count; i++)
            {
                double X = rows[subImages[i].RowNum].CalcX(subImages[i].ColNum);
                double Y = margin;
                for (int j = 0; j < subImages[i].RowNum; j++)
                    Y += spaceBetweenImages + rows[j].Height;

                imagesToShow[i].ViewportWidth = containerAspectRatio / subImages[i].Width;
                animateImage(moveStoryboard, imagesToShow[i], new Point(-(X / subImages[i].Width), -(Y / subImages[i].Width)));	// for animation, use this statement instead of the next one                
                imagesToShow[i].Opacity = 1.0;
            }

            if (ImagesRearranged != null)
            {
                ImagesRearranged(this, EventArgs.Empty);
            }

            // Play Storyboard
            moveStoryboard.Begin();
        }

        private void animateImage(Storyboard moveStoryboard, MultiScaleSubImage currentImage, Point futurePosition)
        {
            // Create Keyframe
            SplinePointKeyFrame endKeyframe = new SplinePointKeyFrame();
            endKeyframe.Value = futurePosition;
            endKeyframe.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(1));

            KeySpline ks = new KeySpline();
            ks.ControlPoint1 = new Point(0, 1);
            ks.ControlPoint2 = new Point(1, 1);
            endKeyframe.KeySpline = ks;

            // Create Animation
            PointAnimationUsingKeyFrames moveAnimation = new PointAnimationUsingKeyFrames();
            moveAnimation.KeyFrames.Add(endKeyframe);

            Storyboard.SetTarget(moveAnimation, currentImage);
            Storyboard.SetTargetProperty(moveAnimation, new PropertyPath("ViewportOrigin"));

            moveStoryboard.Children.Add(moveAnimation);
        }

        Storyboard initStoryboard()
        {
            string key = "moveStoryboard" + storyboardKeyIndex.ToString();

            Storyboard moveStoryboard = new Storyboard();

            msi.Resources.Add(key, moveStoryboard);

            moveStoryboard.Completed += (sender, e) =>
            {
                msi.Resources.Remove(key);
            };
            moveStoryboard.Completed += new EventHandler(moveStoryboard_Completed);
            storyboardKeyIndex++;

            return moveStoryboard;
        }

        /// <summary>
        /// Brings an image up to the centre prominent position on the screen
        /// </summary>
        /// <param name="indexSubImage"></param>
        internal void DisplaySubImageCentered(int indexSubImage, bool fill)
        {
            if (indexSubImage < 0 || indexSubImage >= msi.SubImages.Count)
                return;

            Rect subImageRect = getSubImageRect(indexSubImage);

            double msiAspectRatio = msi.ActualWidth / msi.ActualHeight;

            if (fill)
            {
                DisplaySubImageCentered(indexSubImage, false);
                Point logical = new Point((subImageRect.Left + subImageRect.Right) / 2, ((subImageRect.Top + subImageRect.Bottom) / 2));

                Rect imageRectElement = GetSubImageScreenRect(msi.SubImages[indexSubImage]);
                // Calculate the zoom factor such that the image will fill up the entire screen
                double zoomFactor = (msi.ActualWidth / imageRectElement.Width) < (msi.ActualHeight / imageRectElement.Height) ?
                                                    msi.ActualWidth / imageRectElement.Width : msi.ActualHeight / imageRectElement.Height;
                Zoom(zoomFactor, logical);
            }
            else
            {

                Point newOrigin = new Point(subImageRect.X - (msi.ViewportWidth / 2) + (subImageRect.Width / 2),
                                        subImageRect.Y - ((msi.ViewportWidth / msiAspectRatio) / 2) + (subImageRect.Height / 2));
                msi.ViewportOrigin = newOrigin;
            }
        }

        /// <summary>
        /// Detect if an image was hit at this point
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        int subImageHitTest(Point p)
        {
            for (int i = 0; i < msi.SubImages.Count; i++)
            {
                Rect subImageRect = getSubImageRect(i);
                if (subImageRect.Contains(p))
                    return i;
            }

            return -1;
        }

        /// <summary>
        /// Get the rect for the subimage
        /// </summary>
        /// <param name="indexSubImage"></param>
        /// <returns></returns>
        Rect getSubImageRect(int indexSubImage)
        {
            if (indexSubImage < 0 || indexSubImage >= msi.SubImages.Count)
                return Rect.Empty;

            MultiScaleSubImage subImage = msi.SubImages[indexSubImage];

            return getSubImageRect(subImage);
        }

        Rect getSubImageRect(MultiScaleSubImage subImage)
        {
            double scaleBy = 1 / subImage.ViewportWidth;
            return new Rect(-subImage.ViewportOrigin.X * scaleBy, -subImage.ViewportOrigin.Y * scaleBy,
                                            1 * scaleBy, (1 / subImage.AspectRatio) * scaleBy);
        }

        /// <summary>
        /// Convert a rect to a real point on the screen. Rect starts in MSI special measurements then is converted to real screen measurements.
        /// </summary>
        /// <param name="rect"></param>
        /// <returns></returns>
        Rect getSubImageScreenRect(Rect rect)
        {
            return new Rect(msi.LogicalToElementPoint(new Point(rect.Left, rect.Top)),
                            msi.LogicalToElementPoint(new Point(rect.Right, rect.Bottom)));
        }

        /// <summary>
        /// Get the real position of the image on the screen (not in funny MSI measurement, in real screen coords)
        /// </summary>
        /// <param name="image"></param>
        /// <returns></returns>
        internal Rect GetSubImageScreenRect(MultiScaleSubImage image)
        {
            Rect rect = getSubImageRect(image);
            return getSubImageScreenRect(rect);
        }

        #region Properties


        public Point LastMousePosition
        {
            set
            {
                lastMousePos = value;
            }
        }


        /// <summary>
        /// Raised when the user views an images (or changes from viewing an image to not)
        /// </summary>
        public int? ViewedImage
        {
            get
            {
                return viewedImage;
            }
            set
            {
                viewedImage = value;
            }
        }

        public List<MultiScaleSubImage> ImagesToShow
        {
            get
            {
                return imagesToShow;
            }
        }

        #endregion

        #region EventHandlers

        void moveStoryboard_Completed(object sender, EventArgs e)
        {
            (sender as Storyboard).Completed -= new EventHandler(moveStoryboard_Completed);
            if (ArrangeAnimationComplete != null)
            {
                ArrangeAnimationComplete(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Whenever the view is altered re-examine the images
        /// Detect if an image is being displayed close enough
        /// to show the detail about that image
        /// </summary>        
        void msi_ViewportChanged(object sender, RoutedEventArgs e)
        {
            detectZoomedImage();
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// Ensure the event handlers are cleaned up
        /// </summary>
        public void Dispose()
        {
            if (msi != null)
            {
                msi.ViewportChanged -= new RoutedEventHandler(msi_ViewportChanged);
            }
        }

        #endregion
    }
}
